//--------------------------------------------------------------------------
// $Id: GenericServer.java 12 2006-08-28 05:05:13Z zendragon $
//--------------------------------------------------------------------------
// Copyright 2006 Social Brain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//--------------------------------------------------------------------------

package org.socialbrain.openid.server;

import org.socialbrain.openid.Assertion;
import org.socialbrain.openid.AssociateRequest;
import org.socialbrain.openid.Association;
import org.socialbrain.openid.AuthenticateRequest;
import org.socialbrain.openid.CheckAuthRequest;
import org.socialbrain.openid.CheckAuthResponse;
import org.socialbrain.openid.Constants;
import org.socialbrain.openid.security.DHCrypto;
import org.socialbrain.openid.security.DHKeyPair;
import org.socialbrain.openid.security.Signatory;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.List;
import java.util.Set;

/**
 * @author <a href="http://erning.net/">Zhang Erning</a>
 * @version $Id: GenericServer.java 12 2006-08-28 05:05:13Z zendragon $
 */
public class GenericServer implements Server
{
    protected Association associate(AssociateRequest request)
    {
        Association association = new Association();

        String assocType = request.getAssocType();
        if (!_acceptedAssocType.contains(assocType))
        {
            association.setErrorCode(Constants.ERROR_CODE_UNSUPPORTED_TYPE);
            return association;
        }

        String sessionType = request.getSessionType();
        if (!request.isUnencrypted() && !_acceptedSessionType.contains(sessionType))
        {
            association.setErrorCode(Constants.ERROR_CODE_UNSUPPORTED_TYPE);
            return association;
        }

        DHCrypto dhCrypto = findDhCrypto(assocType, sessionType);
        if (dhCrypto == null)
        {
            association.setErrorCode(Constants.ERROR_CODE_UNSUPPORTED_TYPE);
            return association;
        }

        Signatory signatory = findSignatory(assocType);
        byte[] macKey = signatory.generateKey();

        AssociationSession session = _associationSessionManager.create(assocType, macKey);

        association.setAssocType(assocType);
        association.setSessionType(sessionType);
        association.setAssocHandle(session.getAssocHandle());
        association.setExpireIn(session.getExpiresIn());

        if (request.isUnencrypted())
        {
            association.setMacKey(macKey);
        }
        else
        {
            DHKeyPair dhKeyPair = new DHKeyPair(request.getDhModules(), request.getDhGen());
            byte[] encMacKey = dhCrypto.encrypt(macKey, dhKeyPair, request.getConsumerPublic());
            association.setDhServerPublic(dhKeyPair.getPublicKey());
            association.setEncMacKey(encMacKey);
        }

        return association;
    }

    protected DHCrypto findDhCrypto(String assocType, String sessionType)
    {
        for (DHCrypto dhCrypto : _dhCryptos)
        {
            if (dhCrypto.isAcceptAssocType(assocType) && dhCrypto.isAcceptSessionType(sessionType))
            {
                return dhCrypto;
            }
        }
        return null;
    }

    protected Signatory findSignatory(String assocType)
    {
        for (Signatory signatory : _signatories)
        {
            if (signatory.isAcceptAssocType(assocType))
            {
                return signatory;
            }
        }
        return null;
    }

    //

    protected Assertion approve(AuthenticateRequest request)
    {
        Assertion assertion = new Assertion();

        boolean newSession = false;
        AssociationSession session = _associationSessionManager.lookupByHandle(request.getAssocHandle());
        String assocType = session == null ? _defaultAssocType : session.getAssocType();
        Signatory signatory = findSignatory(assocType);
        if (session == null || session.isExpired())
        {
            newSession = true;
            session = _associationSessionManager.create(assocType, signatory.generateKey());
        }

        // TODO:
        String nonce = "TODO";

        assertion.setIdentity(request.getIdentity());
        assertion.setReturnTo(request.getReturnTo());
        assertion.setNonce(nonce);

        assertion.setAssocHandle(session.getAssocHandle());
        if (newSession)
        {
            assertion.setInvalidateHandle(request.getAssocHandle());
        }

        assertion.setSigned(_SIGNED);
        assertion.setSig(signatory.sign(session.getMacKey(), assertion, _SIGNED_ARRAY));

        return assertion;
    }

    //

    protected Assertion cancel(AuthenticateRequest request)
    {
        boolean immediate = request.isImmediate();
        Assertion assertion = new Assertion(immediate);
        if (immediate)
        {
            // TODO: user_setup_url;
            assertion.setUserSetupUrl("TODO");
        }
        return assertion;
    }

    //

    protected CheckAuthResponse checkAuth(CheckAuthRequest request)
    {
        boolean newSession = false;
        AssociationSession session = _associationSessionManager.lookupByHandle(request.getAssocHandle());
        String assocType = session == null ? _defaultAssocType : session.getAssocType();
        Signatory signatory = findSignatory(assocType);
        if (session == null || session.isExpired())
        {
            newSession = true;
            session = _associationSessionManager.create(assocType, signatory.generateKey());
        }

        CheckAuthResponse response = new CheckAuthResponse(checkSig(signatory, session, request));
        if (newSession)
        {
            response.setInvalidateHandle(request.getAssocHandle());
        }
        return response;
    }

    protected boolean checkSig(Signatory signatory, AssociationSession session, CheckAuthRequest request)
    {
        _logger.debug("Calculating signature");

        String[] signed = StringUtils.split(request.getSigned(), ',');
        byte[] csig = signatory.sign(session.getMacKey(), request, signed);
        if (csig == null) return false;

        byte[] asig = request.getSig();

        if (_logger.isDebugEnabled())
        {
            _logger.debug(String.format("Calculated sig [%s], assertion sig [%s]",
                    new String(Hex.encodeHex(csig)), new String(Hex.encodeHex(asig))));
        }

        if (csig.length != asig.length) return false;
        for (int i = 0; i < csig.length; i++)
        {
            if (csig[i] != asig[i]) return false;
        }
        return true;
    }

    //

    public void setAcceptedAssocType(Set<String> value)
    {
        _acceptedAssocType = value;
    }

    public Set<String> getAcceptedAssocType()
    {
        return _acceptedAssocType;
    }

    public void setAcceptedSessionType(Set<String> value)
    {
        _acceptedSessionType = value;
    }

    public Set<String> getAcceptedSessionType()
    {
        return _acceptedSessionType;
    }

    private Set<String> _acceptedAssocType;
    private Set<String> _acceptedSessionType;

    //

    public void setDefaultAssocType(String value)
    {
        _defaultAssocType = value;
    }

    public String getDefaultAssocType()
    {
        return _defaultAssocType;
    }

    public void setDefaultSessionType(String value)
    {
        _defaultSessionType = value;
    }

    public String getDefaultSessionType()
    {
        return _defaultSessionType;
    }

    private String _defaultAssocType = Constants.DEFAULT_ASSOC_TYPE;
    private String _defaultSessionType = Constants.DEFAULT_SESSION_TYPE;

    //

    public void setDhKeyPair(DHKeyPair value)
    {
        _dhKeyPair = value;
    }

    public DHKeyPair getDHKeyPair()
    {
        return _dhKeyPair;
    }

    public void setDhCryptos(List<DHCrypto> value)
    {
        _dhCryptos = value;
    }

    public List<DHCrypto> getDhCryptos()
    {
        return _dhCryptos;
    }

    private DHKeyPair _dhKeyPair;
    private List<DHCrypto> _dhCryptos;

    //

    public void setSignatories(List<Signatory> value)
    {
        _signatories = value;
    }

    public List<Signatory> getSignatories()
    {
        return _signatories;
    }

    private List<Signatory> _signatories;

    //

    public void setAssociationSessionManagerr(AssociationSessionManager value)
    {
        _associationSessionManager = value;
    }

    public AssociationSessionManager getAssociationSessionManager()
    {
        return _associationSessionManager;
    }

    private AssociationSessionManager _associationSessionManager;

    private static final String _SIGNED = String.format("%s,%s,%s",
            Constants.PARAM_IDENTITY, Constants.PARAM_RETURN_TO, Constants.PARAM_NONCE);
    private static final String[] _SIGNED_ARRAY = StringUtils.split(_SIGNED, ',');

    private transient static final Log _logger = LogFactory.getLog(GenericServer.class);
}
